package com.m1.tzolkin.ia;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map.Entry;
import java.util.SortedMap;
import java.util.TreeMap;

import com.m1.tzolkin.model.IAction;
import com.m1.tzolkin.model.TZResource;
import com.m1.tzolkin.model.TzolkinConstants;
import com.m1.tzolkin.model.board.TZBoard;
import com.m1.tzolkin.model.exceptions.EndOfGameException;
import com.m1.tzolkin.model.exceptions.IllegalWorkerOperationException;
import com.m1.tzolkin.model.exceptions.NoMovedWorkersException;
import com.m1.tzolkin.model.exceptions.NoPlaceException;
import com.m1.tzolkin.model.exceptions.NotEnoughCornException;
import com.m1.tzolkin.model.exceptions.NotEnoughWorkersException;
import com.m1.tzolkin.model.exceptions.StepMoveException;
import com.m1.tzolkin.model.exceptions.WaitingOperationsException;
import com.m1.tzolkin.model.players.ITZPlayer;
import com.m1.tzolkin.model.players.IWorker;
import com.m1.tzolkin.model.technos.Technologies;
import com.m1.tzolkin.model.temples.ITemple;
import com.m1.tzolkin.model.temples.TempleEnum;
import com.m1.tzolkin.model.wheels.IWheel;
import com.m1.tzolkin.model.wheels.WheelEnum;
import com.m1.tzolkin.model.wheels.WheelEnum.PalenqueAction;

public class Temple_IA implements IA {
	// Plateau de jeu pour connaître l'état de la partie
	private TZBoard board;
	private ITZPlayer me;

	// Attributs pour determiner les actions à réaliser
	private static int SPENDING_MAX_CORN = 4;
	private int NEEDED_CORN;
	private int spentCorn;
	private boolean haveToPlaceWorker;
	//private int previ_ressource = 0;


	public Temple_IA(TZBoard board)  {
		this.board = board;

		//TODO : amélioration possible
		//previ_ressource = getTotalRessources();
	}

	/********************************************************************/
	/**							Requetes								*/
	/********************************************************************/

	public void play() {
		// Initialisation
		me = board.getCurrentPlayer();
		System.out.println(me.getName());
		NEEDED_CORN = me.getTotalPawns().size() * TzolkinConstants.WORKERS_HUNGRY;
		int day = board.getTurn();
		spentCorn = 0;

		// Strategie normale
		if (day < 25) {
			/**  Retirer ouvrier  **/

			// TECHNO
			upTechnoIfPossible();

			// BOIS
			retrieveWoodIfNeeded();

			// MAIS
			retrieveCornIfNeeded();

			// DEFAULT
			defaultTakeAction();

			// Récuperer les pions eventuellement placés de force
			retrieveForcedPlacedWorkers();


			/**  Poser ouvrier  **/

			regularPlaceWorker();
			
			if (haveToPlaceWorker) {
				forcedPlaceWorker();
			}

		// Strategie lors de l'avant dernier tour
		} else if (day == 25) {
			for (int p = 0; p < me.getHandPawns().size(); ++p) {
				try {
					board.placeWorker(WheelEnum.PALENQUE);
				} catch (Exception e) {
					try {
						board.placeWorker(WheelEnum.UXMAL);
					} catch (Exception e2) {
						// Ne rien faire
					}
				}
			}

		// Strategie du dernier tour
		} else {
			retrieveAllWorkers(me);
		}

		try {
			System.out.println("Fin tour bot");
			board.endTurn();
		} catch (WaitingOperationsException e) {
			e.printStackTrace();
		} catch (EndOfGameException e) {
			e.printStackTrace();
		} catch (NoMovedWorkersException e) {
			e.printStackTrace();
		}
	}
	
	/********************************************************************/
	/**					Méthodes servant à faire jouer l'IA				*/
	/********************************************************************/

	/**
	 *  Retire l'ouvrier sur Tikal pour monter en technologie agriculture si on peut.
	 */
	private void upTechnoIfPossible() {
		WheelEnum tikal = WheelEnum.TIKAL;
		int actualTechLevel = board.getTechLevel(Technologies.AGRICULTURE, me);
		IWorker w = board.getWheel(tikal).getWorker(3);
		int neededRessources = determineNeededRessources(actualTechLevel);
		boolean hasEnoughRessources = getTotalRessources() >= neededRessources;

		// Si on a un ouvrier sur la case d'indice 3 de la roue de tikal, et que l'on a suffisament de ressource, on le retire
		if (hasEnoughRessources && w != null && w.getOwner() != null && w.getOwner().equals(me)) {
			HashMap<IAction, Integer> mapAction;
			try {
				mapAction = (HashMap<IAction, Integer>) board.takeWorker(tikal, 3);
	
				int lastAction = mapAction.keySet().size();
				int i = 0;
	
				// Execute l'action de monter 2 fois dans la techno agriculture
				for (IAction a : mapAction.keySet()) {
					if (i == lastAction) {
						try {
							board.doAction(a);
							for (int up = 0; up < 2; ++up) {
								int bill = board.wannaUpTech(Technologies.AGRICULTURE);
								payBill(bill);
								board.upTech(Technologies.AGRICULTURE, me);
	
								// Monte dans le meilleur choix de temple si on peut
								upInBestTemple();
							}
						} catch (Exception e) {
							e.printStackTrace();
						}
						break;
					}
					++i;
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * Si on a pas assez de bois pour payer la succession de 2 prochaines upgrade, on essaie d'en prendre sur palenque.
	 */
	private void retrieveWoodIfNeeded() {
		WheelEnum tikal = WheelEnum.TIKAL;
		int actualTechLevel = board.getTechLevel(Technologies.AGRICULTURE, me);
		int neededRessources = determineNeededRessources(actualTechLevel);
		boolean hasEnoughRessources = getTotalRessources() >= neededRessources;

		// Si on a besoin de ressource pour la prochaine upgrade en techno, on essaie de prendre du bois sur palenque
		if (!hasEnoughRessources) {
			IWheel palenque = board.getWheel(WheelEnum.PALENQUE);
			PalenqueAction[] actionTab = new PalenqueAction[palenque.getActions().length - 1];
			for (int c = 1; c < palenque.getActions().length - 1; ++c) {
				actionTab[c] = (PalenqueAction) palenque.getActions()[c];
			}
			System.out.println("HERE");

			// Boucle sur les ouvriers posés sur Palenque, en partant du plus haut, tant qu'on a besoin de ressource
			for (int c = palenque.getCircuitLength(); c > 0 && !hasEnoughRessources ; --c) {
				IWorker worker = palenque.getWorker(c);
				if (worker != null && worker.getOwner() != null && worker.getOwner().equals(me)
						&& (actionTab[c].getTileNb(PalenqueAction.WOOD_TILE, board) >= 1)) {

					// On retire l'ouvrier le plus haut s'il y a une tuile de bois sur sa case
					HashMap<IAction, Integer> mapAction;
					try {
						mapAction = (HashMap<IAction, Integer>) board.takeWorker(tikal, c);
						int lastAction = mapAction.keySet().size();
						int i = 0;
	
						// On execute l'action la case la plus haute (= la case de l'ouvrier)
						for (IAction a : mapAction.keySet()) {
							if (i == lastAction) {
								try {
									board.doAction(a);
	
									// Execute l'action de récuperer le bois
									HashMap<IAction, Integer> choice = (HashMap<IAction, Integer>) board.getActions();
									for (IAction actionTile : choice.keySet()) {
										if (actionTile.toString().contains("WOOD")) {
											//int previousStock = getTotalRessources();
											board.doAction(actionTile);
											//previ_ressource += (getTotalRessources() - previousStock);
										}
									}
	
									// Met à jour le bouléen pour la boucle
									hasEnoughRessources = getTotalRessources() >= neededRessources;
								} catch (Exception e) {
									e.printStackTrace();
								}
							}
						}
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			}
		}
	}

	/**
	 * Si on a pas assez de maïs pour passer le jour de festin, on va essayer d'en récuperer.
	 */
	private void retrieveCornIfNeeded( ) {
		boolean hasEnoughCorn = me.getCorn() < NEEDED_CORN;

		// Si on a pas assez de maïs pour payer le prochain jour de festin, on va essayer d'en récuperer sur palenque
		if (!hasEnoughCorn) {
			IWheel palenque = board.getWheel(WheelEnum.PALENQUE);

			// Cherche l'ouvrier le plus haut sur la roue de Palenque
			for (int c = palenque.getCircuitLength(); c > 0 && !hasEnoughCorn ; --c) {
				IWorker worker = palenque.getWorker(c);
				if (worker != null && worker.getOwner() != null && worker.getOwner().equals(me)) {

					// Test si on peut récupérer du maïs sans avoir à bruler une forêt si jamais le maïs il n'y a pas de tuile de foret
					boolean canRetrieveCorn = board.getTechLevel(Technologies.AGRICULTURE, me) >= 2 || c == 1;

					// Si on ne peut pas en récupérer automatiquement, on regarde si il y a une tuile de disponible sur cette case
					if (!canRetrieveCorn) {
						PalenqueAction[] actionTab = (PalenqueAction[]) palenque.getActions();
						canRetrieveCorn = actionTab[c].getTileNb(PalenqueAction.CORN_TILE, board) >= 1;
					}

					// Si on peut récupérer du maïs, alors on retire l'ouvrier de cette case la plus haute pour en prendre.
					if (canRetrieveCorn) {
						try {
							HashMap<IAction, Integer> choice = (HashMap<IAction, Integer>) board.takeWorker(WheelEnum.PALENQUE, c);
							//TODO : Verifier si ça fonctionne
							for (Entry<IAction, Integer> set : choice.entrySet()) {
								if (set.getValue() == 0 && set.getKey().getType().toString().contains("CORN")) {
									board.doAction(set.getKey());
								}
							}
						} catch (Exception e) {
							e.printStackTrace();
						}
					}
				}
			}
			
		}
	}
	
	/**
	 * Actions de l'IA par défaut, si elle n'a aucun ouvrier dans la mains et doit retirer un pion.
	 */
	private void defaultTakeAction() {
		// Si l'IA n'a pas de joueur en main, il faut en récupérer un.
		if (me.getHandPawns().size() == 0) {
			int highestSquareIndiceWithWood = this.getHighestSquareWithWoodTile(me);
			WheelEnum palenque = WheelEnum.PALENQUE;
			HashMap<IAction, Integer> choice;

			// Determine si on a plus besoin de bois ou de maïs.
			boolean takeWood = me.getCorn() >= NEEDED_CORN * 2 && highestSquareIndiceWithWood != 0;

			if(takeWood) {
				// Execute l'action de récuperer le bois si on en trouve.
				try {
					// TODO : check
					choice = (HashMap<IAction, Integer>) board.takeWorker(palenque, highestSquareIndiceWithWood);
					for (Entry<IAction, Integer> set : choice.entrySet()) {
						if (set.getValue() == 0 && set.getKey().getType().toString().contains("WOOD")) {
							board.doAction(set.getKey());
							//previ_ressource += (getTotalRessources() - previousStock);
						}
					}
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
			else {
				// Execute l'action de prendre du maïs.
				// TODO : check
				int highestSquareIndice = this.getHighestSquare(me);
				try {
					choice = (HashMap<IAction, Integer>) board.takeWorker(palenque, highestSquareIndice);

					// On récupère du maïs
					for (Entry<IAction, Integer> set : choice.entrySet()) {
						if (set.getValue() == 0 && set.getKey().getType().toString().contains("CORN")) {
							board.doAction(set.getKey());
						}
					}
				} catch (Exception e) {
					// Ne rien faire
				}
			}			
		}
	}

	/**
	 * Récupère les pions qui ont eventuellement été posé de force par la méthode
	 * forcedPlaceWorker();
	 */
	private void retrieveForcedPlacedWorkers() {
		ArrayList<WheelEnum> wheelList = new ArrayList<WheelEnum>();
		wheelList.add(WheelEnum.YAXCHILAN);
		wheelList.add(WheelEnum.UXMAL);
		wheelList.add(WheelEnum.CHICHEN_ITZA);

		for (WheelEnum wheel : wheelList) {
			for (int i = 1; i < 3; ++i) {
				try {
					HashMap<IAction, Integer> choice = (HashMap<IAction, Integer>) board.takeWorker(wheel, i);

					/* Dans le cas où on reprend un ouvrier sur yaxchilan,
					on effectue une action au hasard pour récuperer des ressources.
					Sinon, on ne fait rien. */
					if (board.getActions().size() >= 1 && wheel.equals(WheelEnum.YAXCHILAN)) {
						for (Entry<IAction, Integer> set : choice.entrySet()) {
							// On execute une des actions les plus hautes
							if (set.getValue() == 0) {
								board.doAction(set.getKey());
								break;
							}
						}
					}
				} catch (Exception e) {
					// Ne rien faire, juste tester le suivant
				}
			}
		}
	}

	/***** Posage d'ouvrier ******/
	
	/**
	 * Action standard de poser un ouvrier.
	 */
	private void regularPlaceWorker() {
		// Place un ouvrier sur tikal si on a suffisamment de ressource pour payer les 2 prochaines upgrade
		int actualTechLevel = board.getTechLevel(Technologies.AGRICULTURE, me);
		int neededRessources = determineNeededRessources(actualTechLevel);

		if (getTotalRessources() >= neededRessources) {
			IWheel tikal = board.getWheel(WheelEnum.TIKAL);
			for (int c = 0; c < 3; ++c) {
				int calculatedPrice = calculateCornPrice(tikal, c);
				if ((tikal.getWorker(c) == null) &&
						((spentCorn + calculatedPrice) < SPENDING_MAX_CORN)) {
					try {
						board.placeWorker(WheelEnum.TIKAL);
						spentCorn += calculatedPrice;
						haveToPlaceWorker = false;
					} catch (NoPlaceException e) {
						// Ne rien faire
					} catch (NotEnoughCornException e) {
						// Ne rien faire
					} catch (IllegalWorkerOperationException e) {
						e.printStackTrace();
					} catch (NotEnoughWorkersException e) {
						e.printStackTrace();
					}
				}
			}
		}

		// Sinon, on en place un sur palenque, si possible
		else {
			IWheel palenque = board.getWheel(WheelEnum.PALENQUE);
			boolean stop = false;
			while (!stop) {
				int indice = -1;
				try {
					indice = palenque.getMinimalPosition();
				} catch (NoPlaceException e) {
					// Rien à faire
				}

				int calculatedPrice = calculateCornPrice(palenque, indice);
				stop = (indice == -1) || (spentCorn +  calculatedPrice < SPENDING_MAX_CORN);
				if (!stop) {
					try {
						board.placeWorker(WheelEnum.PALENQUE);
						spentCorn += calculatedPrice;
						haveToPlaceWorker = false;
					} catch (NoPlaceException e) {
						e.printStackTrace();
					} catch (NotEnoughCornException e) {
						stop = true;
					} catch (IllegalWorkerOperationException e) {
						e.printStackTrace();
					} catch (NotEnoughWorkersException e) {
						e.printStackTrace();
					}
				}
			}
		}
	}

	/** Action de placer un ouvrier, dans le cas où on est forcé à le faire.
	 * Si le joueur ne peut ni retirer d'ouvrier, et qu'aucune case prévue dans
	 * l'algorithme "regularPlaceWorker" n'est accessible, on arrive ici pour être sur
	 * que l'IA fait au moins une action.
	 */
	private void forcedPlaceWorker() {
		boolean placed = false;

		// Si la case 1er joueur n'est pas interessante à prendre (ou prise), 
		// on essaie d'abord de poser un ouvrier sur Palenque.
		if (board.getCorn() < 3 || board.getFirstWorker() != null) {
			try {
				board.placeWorker(WheelEnum.PALENQUE);
				placed = true;
			} catch (NoPlaceException e) {
				e.printStackTrace();
			} catch (NotEnoughCornException e) {
				e.printStackTrace();
			} catch (IllegalWorkerOperationException e) {
				e.printStackTrace();
			} catch (NotEnoughWorkersException e) {
				e.printStackTrace();
			}
		}

		// Si le posage sur palenque a échouée, ou que la case est interessante,
		// on essaie de prendre la case PJ.
		if (!placed) {
			try {
				board.placeFirstWorker();
				placed = true;
			} catch (NoPlaceException e) {
				// S'il n'y a pas de place, on ne fait rien
			} catch (NotEnoughCornException e) {
				// Si on a pas assez de maïs pour poser son pion, alors on doit quémander du maïs.
				board.askForCorn();
				forcedPlaceWorker();
				placed = true;
			} catch (NotEnoughWorkersException e) {
				e.printStackTrace();
			} catch (IllegalWorkerOperationException e) {
				e.printStackTrace();
			}
		}

		// Si aucune option précédente n'a fonctionnée, alors on pose son ouvrier "au hasard".
		if (!placed) {
			ArrayList<WheelEnum> wheelList = new ArrayList<WheelEnum>();
			wheelList.add(WheelEnum.YAXCHILAN);
			wheelList.add(WheelEnum.TIKAL);
			wheelList.add(WheelEnum.UXMAL);
			wheelList.add(WheelEnum.CHICHEN_ITZA);

			for (WheelEnum wheel : wheelList) {
				try {
					board.placeWorker(wheel);
					placed = true;
					break;
				} catch (NoPlaceException e) {
					// Ne rien faire
				} catch (NotEnoughCornException e) {
					// Ne rien faire
				} catch (IllegalWorkerOperationException e) {
					e.printStackTrace();
				} catch (NotEnoughWorkersException e) {
					e.printStackTrace();
				}
			}
		}

		// Si on arrive ici sans avoir posé de pion... Y a comme un problème.
	}

	/**
	 * Méthode récupérant tout les joueurs posé sur le plateau.
	 */
	private void retrieveAllWorkers(ITZPlayer player) {
		for (WheelEnum wheelEnum : WheelEnum.values()) {
			IWheel currentWheel =  board.getWheel(wheelEnum);
			for (int c = 0; c < currentWheel.getCircuitLength(); ++c) {
				try {
					board.takeWorker(wheelEnum, c);
				} catch (Exception e) {
					// Ne rien faire
				}
			}
		}
	}

	/********************************************************************/
	/**							 Outils									*/
	/********************************************************************/

	/** Retourne l'indice de la case la plus haute accessible par un joueur. */
	private int getHighestSquare(ITZPlayer player) {
		IWheel palenque = board.getWheel(WheelEnum.PALENQUE);

		for (int c = palenque.getActions().length; c > 0  ; --c) {
			IWorker worker = palenque.getWorker(c);
			if (worker != null && worker.getOwner() != null && worker.getOwner().equals(player)) {
					return c;
			}
		}

		return 0;
	}
	

	/** Retourne l'indice de la case la plus haute accessible contenant une tuile de bois. */
	private int getHighestSquareWithWoodTile(ITZPlayer player) {
		IWheel palenque = board.getWheel(WheelEnum.PALENQUE);
		PalenqueAction[] actionTab = (PalenqueAction[]) palenque.getActions();

		// Cherche l'ouvrier le plus haut sur la roue de Palenque
		for (int c = palenque.getActions().length; c > 0  ; --c) {
			IWorker worker = palenque.getWorker(c);
			if (worker != null && worker.getOwner() != null && worker.getOwner().equals(player)
					&& (actionTab[c].getTileNb(PalenqueAction.WOOD_TILE, board) >= 1)) {
					return c;
			}
		}

		return 0;
	}

	/**
	 * Nombre total de ressource (bois, pierre, or). */
	private int getTotalRessources() {
		int total = 0;
		for (TZResource res : TZResource.values()) {
			total += me.getWallet().getResources(res);
		}
		return total;
	}

	/**
	 * Paie le coût en ressource, en privilégiant l'utilisation des ressources les moins onéreuses.
	 * @param bill
	 */
	private void payBill(int bill) {
		// Créer une sortedMap pour avoir la liste des ressource dans l'ordre croissant
		SortedMap<TZResource, Integer> sortedMap = new TreeMap<TZResource, Integer>(
				new Comparator<TZResource>() {
					public int compare(TZResource arg0, TZResource arg1) {
						double res = arg0.getAbsoluteValue() - arg1.getAbsoluteValue();
						return (int) (res);
					}
				});
		//TODO : vérifier l'ordre des ressources choisies

		for (TZResource r : TZResource.values()) {
			sortedMap.put(r, new Integer((int) r.getAbsoluteValue()));
		}

		// Paie le cout en ressource en utilisant en priorite les ressources peu cher
		for (TZResource r : sortedMap.keySet()) {
			if (bill > 0) {
				board.pay(r);
				--bill;
			} else {
				break;
			}
		}
	}


	/**
	 * Determine le nombre de ressource necessaire pour l'upgrade de 2 niveaux de technologie consécutifs en agriculture.
	 * @param actualTechLevel
	 * @return
	 */
	private int determineNeededRessources(int actualTechLevel) {
		int firstUpgrade = (actualTechLevel % 3) + 1;
		int secondUpgrade = ((actualTechLevel + 1) % 3) + 1;
		return firstUpgrade + secondUpgrade;
	}

	/**
	 * Fait monter le joueur dans le temple le plus avantageux.
	 * @param temples : La liste des temples du plateau.
	 */
	private void upInBestTemple() {
		/* Créer la liste en mettant en priorité les temples permettant de gagner
		 * le plus de point de victoire. */
		ArrayList<ITemple> templeList = new ArrayList<ITemple>();
		templeList.add(TempleEnum.KUKULCAN);
		templeList.add(TempleEnum.QUETZALCOATL);
		templeList.add(TempleEnum.CHAAC);

		// Parcours les temples uppable
		for (ITemple t : templeList) {
			
			if (board.canUpTemple(t, me)) {
				//TODO
				// Regarde s'il y a un joueur sur la même case que nous, ou au dessus
				int ownPosition = board.getTemplePosition(t, me);
				for (ITZPlayer p : board.getPlayers()) {
					if ((!p.equals(me)) &&
						(board.getTemplePosition(t, p) == ownPosition ||
						board.getTemplePosition(t, p) == ownPosition + 1)) {
						// Monte dans ce temple si c'est le cas
						try {
							board.upTemple(me, t);
						} catch (StepMoveException e) {
							e.printStackTrace();
						}
					}
				}
			}
		}

		/* Si aucun temple ne correspond aux demande du dessus,
		 * alors on monte dans n'importe lequel  */
		for (ITemple t : templeList) {
			try {
				board.upTemple(me, t);
			} catch (StepMoveException e) {
				// Ne rien faire si on ne peut pas monter dans le temple courant
			}
		}
	}

	/** Retourne le coût de posage d'un ouvrier sur une roue.
	 * 
	 * @param wheel La roue où on veut poster l'ouvrier
	 * @param caseIndice L'indice d'insertion
	 * @return Coût posage
	 */
	private int calculateCornPrice(IWheel wheel, int caseIndice) {
		// Un malus selon le nombre de pion déjà placé est appliqué.
		return caseIndice + board.getPlacedWorkers();
	}

}
